<?php

/**
 * 
 * 微信认证基础类
 *
 */

namespace Org\Wechat;

class WechatAuth{
  /* 消息类型常量 */

  const MSG_TYPE_TEXT = 'text';
  const MSG_TYPE_IMAGE = 'image';
  const MSG_TYPE_VOICE = 'voice';
  const MSG_TYPE_VIDEO = 'video';
  const MSG_TYPE_MUSIC = 'music';
  const MSG_TYPE_NEWS = 'news';
  const MSG_TYPE_LOCATION = 'location';
  const MSG_TYPE_LINK = 'link';
  const MSG_TYPE_EVENT = 'event';

  /* 二维码类型常量 */
  const QR_SCENE = 'QR_SCENE';
  const QR_LIMIT_SCENE = 'QR_LIMIT_SCENE';

  /**
   * 微信开发者申请的appID
   * @var string
   */
  private $appId = '';

  /**
   * 微信开发者申请的appSecret
   * @var string
   */
  private $appSecret = '';

  /**
   * 获取到的access_token
   * @var string
   */
  private $accessToken = '';

  /**
   * 微信api根路径
   * @var string
   */
  private $apiURL = 'https://api.weixin.qq.com/cgi-bin';

  /**
   * 微信媒体文件根路径
   * @var string
   */
  private $mediaURL = 'http://file.api.weixin.qq.com/cgi-bin';

  /**
   * 微信二维码根路径
   * @var string
   */
  private $qrcodeURL = 'https://mp.weixin.qq.com/cgi-bin';
  private $requestCodeURL = 'https://open.weixin.qq.com/connect/oauth2/authorize';
  private $oauthApiURL = 'https://api.weixin.qq.com/sns';
  private $getJsTicketURL = 'https://api.weixin.qq.com/cgi-bin/ticket';
  
  private $sendTplMsgURL = 'https://api.weixin.qq.com/cgi-bin/message/template/send';
  private $getQrcodeTicketUrl = 'https://api.weixin.qq.com/cgi-bin/qrcode/create';

  /**
   * 构造方法，调用微信高级接口时实例化SDK
   * @param string $appid  微信appid
   * @param string $secret 微信appsecret
   * @param string $token  获取到的access_token
   */
  public function __construct($appid, $secret, $token = null){
    if($appid && $secret){
      $this->appId = $appid;
      $this->appSecret = $secret;
      if(!empty($token)){
        $this->accessToken = $token;
      }
    }else{
      throw new \Exception('请您先配置微信app_id和secret！');
    }
  }

  public function getRequestCodeURL($redirect_uri, $state = null, $scope = 'snsapi_userinfo'){
    $query = array(
      'appid' => $this->appId,
      'redirect_uri' => $redirect_uri,
      'response_type' => 'code',
      'scope' => $scope,
    );

    if(!is_null($state) && preg_match('/[a-zA-Z0-9]+/', $state)){
      $query['state'] = $state;
    }

    $query = http_build_query($query);
    return "{$this->requestCodeURL}?{$query}#wechat_redirect";
  }

  /**
   * 获取JS票据
   * @author Max.Yu <max@jipu.com>
   */
  public function getJsapiTicket(){
    if(!S('jsapiTicket')){
      $info = $this->getAccessToken();
      if(is_array($info) && $info['access_token']){
        $this->accessToken = $info['access_token'];
      }
      $param = array(
        'access_token' => $this->accessToken,
        'type' => 'jsapi'
      );
      $info = self::http("{$this->getJsTicketURL}/getticket", $param);
      $jsapiTicket = json_decode($info, true);
      S('jsapiTicket', $jsapiTicket, 7000);
    }else{
      $jsapiTicket = S('jsapiTicket');
    }
    return $jsapiTicket;
  }

  /**
   * 获取access_token，用于后续接口访问
   * @return array access_token信息，包含 token 和有效期
   */
  public function getAccessToken($type = 'client', $code = null){
    $param = array(
      'appid' => $this->appId,
      'secret' => $this->appSecret
    );
    $cacheName = 'accessToken_'.$type;
    //授权access_token
    if($type == 'code'){
      if(session($cacheName)){
        $s_token = session($cacheName);
        $url = "{$this->oauthApiURL}/auth";
        $arr = array(
          'access_token' => $s_token['access_token'],
          'openid' => $s_token['openid'],
        );
        $token_ = self::http($url, $arr);
        $token = json_decode($token_, true);
        if($token['errcode'] == 0){
          $this->accessToken = $s_token['access_token'];
          return $s_token;
        }else{
          session($cacheName, null);
        }
      }
      if(empty($code)){
        if(C('WECHAT_USERINFO_BY_API') == true){
          $access_token = A('User', 'Event')->getAuthAccessTokenByApi();
          $this->accessToken = $access_token['access_token'];
          session($cacheName, $access_token);
          return $access_token;
        }else{
          return array();
        }
      }
    }
    switch($type){
      case 'client':
        $param['grant_type'] = 'client_credential';
        $url = "{$this->apiURL}/token";
        break;
      case 'code':
        $param['code'] = $code;
        $param['grant_type'] = 'authorization_code';
        $url = "{$this->oauthApiURL}/oauth2/access_token";
        break;
      default:
        throw new \Exception('不支持的grant_type类型！');
        break;
    }
    //缓存不存在、生成
    if(S($cacheName) && $type == 'client'){
      $token = S($cacheName);
      $this->accessToken = $token['access_token'];
      return $token;
    }else{
      $token_ = self::http($url, $param);
      $token = json_decode($token_, true);
      if(is_array($token)){
        if(isset($token['errcode'])){
          throw new \Exception($token['errmsg']);
        }else{
          $this->accessToken = $token['access_token'];
          //保存到缓存
          if($type == 'client'){
            S($cacheName, $token, $token['expires_in']);
          }else{
            session($cacheName, $token);
          }
          return $token;
        }
      }else{
        throw new \Exception('获取微信access_token失败！');
      }
    }
  }

  /**
   * 获取授权用户信息
   * @param string $token acess_token
   * @param string $lang  指定的语言
   * @return array         用户信息数据，具体参见微信文档
   */
  public function getUserInfo($token, $lang = 'zh_CN'){
    $query = array(
      'access_token' => $token['access_token'],
      'openid' => $token['openid'],
      'lang' => $lang,
    );

    $info = self::http("{$this->oauthApiURL}/userinfo", $query);
    return json_decode($info, true);
  }

  /**
   * 上传媒体资源
   * @param string $filename 媒体资源本地路径
   * @param string $type     媒体资源类型，具体请参考微信开发手册
   */
  public function mediaUpload($filename, $type){
    $param = array(
      'access_token' => $this->accessToken,
      'type' => $type
    );

    $filename = realpath($filename);
    if(!$filename){
      throw new \Exception('资源路径错误！');
    }

    $file = array('media' => "@{$filename}");
    $url = "{$this->mediaURL}/media/upload";
    $data = self::http($url, $param, $file, 'POST');

    return json_decode($data, true);
  }

  /**
   * 获取媒体资源下载地址
   * 注意：视频资源不允许下载
   * @param string $media_id 媒体资源id
   * @return string 媒体资源下载地址
   */
  public function mediaGet($media_id){
    $param = array(
      'access_token' => $this->accessToken,
      'media_id' => $media_id
    );

    $url = "{$this->mediaURL}/media/get?";
    return $url.http_build_query($param);
  }

  /**
   * 给指定用户推送信息
   * 注意：微信规则只允许给在48小时内给公众平台发送过消息的用户推送信息
   * @param string $openid  用户的openid
   * @param array  $content 发送的数据，不同类型的数据结构可能不同
   * @param string $type 推送消息类型
   */
  public function messageCustomSend($openid, $content, $type = self::MSG_TYPE_TEXT){

    //基础数据
    $data = array(
      'touser' => $openid,
      'msgtype' => $type,
    );

    //根据类型附加额外数据
    $data[$type] = call_user_func(array(self, $type), $content);

    return $this->api('message/custom/send', $data);
  }

  /**
   * 发送文本消息
   * @param string $openid 用户的openid
   * @param string $text   发送的文字
   */
  public function sendText($openid, $text){
    return $this->messageCustomSend($openid, $text, self::MSG_TYPE_TEXT);
  }

  /**
   * 发送图片消息
   * @param string $openid 用户的openid
   * @param string $media  图片ID
   */
  public function sendImage($openid, $media){
    return $this->messageCustomSend($openid, $media, self::MSG_TYPE_IMAGE);
  }

  /**
   * 发送语音消息
   * @param string $openid 用户的openid
   * @param string $media  音频ID
   */
  public function sendVoice($openid, $media){
    return $this->messageCustomSend($openid, $media, self::MSG_TYPE_VOICE);
  }

  /**
   * 发送视频消息
   * @param string $openid      用户的openid
   * @param string $media_id    视频ID
   * @param string $title       视频标题
   * @param string $discription 视频描述
   */
  public function sendVideo(){
    $video = func_get_args();
    $openid = array_shift($video);
    return $this->messageCustomSend($openid, $video, self::MSG_TYPE_VIDEO);
  }

  /**
   * 发送音乐消息
   * @param string $openid         用户的openid
   * @param string $title          音乐标题
   * @param string $discription    音乐描述
   * @param string $musicurl       音乐链接
   * @param string $hqmusicurl     高品质音乐链接
   * @param string $thumb_media_id 缩略图ID
   */
  public function sendMusic(){
    $music = func_get_args();
    $openid = array_shift($music);
    return $this->messageCustomSend($openid, $music, self::MSG_TYPE_MUSIC);
  }

  /**
   * 发送图文消息
   * @param string $openid 用户的openid
   * @param array  $news   图文内容 [标题，描述，URL，缩略图]
   * @param array  $news1  图文内容 [标题，描述，URL，缩略图]
   * @param array  $news2  图文内容 [标题，描述，URL，缩略图]
   *                ...     ...
   * @param array  $news9  图文内容 [标题，描述，URL，缩略图]
   */
  public function sendNews(){
    $news = func_get_args();
    $openid = array_shift($news);
    return $this->messageCustomSend($openid, $news, self::MSG_TYPE_NEWS);
  }

  /**
   * 发送一条图文消息
   * @param string $openid      用户的openid
   * @param string $title       文章标题
   * @param string $discription 文章简介
   * @param string $url         文章连接
   * @param string $picurl      文章缩略图
   */
  public function sendNewsOnce(){
    $news = func_get_args();
    $openid = array_shift($news);
    $news = array($news);
    return $this->messageCustomSend($openid, $news, self::MSG_TYPE_NEWS);
  }

  /**
   * 创建用户组
   * @param string $name 组名称
   */
  public function groupsCreate($name){
    $data = array('group' => array('name' => $name));
    return $this->api('groups/create', $data);
  }

  /**
   * 查询所有分组
   * @return array 分组列表
   */
  public function groupsGet(){
    return $this->api('groups/get', '', 'GET');
  }

  /**
   * 查询用户所在的分组
   * @param string $openid 用户的OpenID
   * @return number         分组ID
   */
  public function groupsGetid($openid){
    $data = array('openid' => $openid);
    return $this->api('groups/getid', $data);
  }

  /**
   * 修改分组
   * @param number $id   分组ID
   * @param string $name 分组名称
   * @return array        修改成功或失败信息
   */
  public function groupsUpdate($id, $name){
    $data = array('id' => $id, 'name' => $name);
    return $this->api('groups/update', $data);
  }

  /**
   * 移动用户分组
   * @param string $openid     用户的OpenID
   * @param number $to_groupid 要移动到的分组ID
   * @return array              移动成功或失败信息
   */
  public function groupsMemberUpdate($openid, $to_groupid){
    $data = array('openid' => $openid, 'to_groupid' => $to_groupid);
    return $this->api('groups/member/update', $data);
  }

  /**
   * 用户设备注名
   * @param string $openid 用户的OpenID
   * @param string $remark 设备注名
   * @return array          执行成功失败信息
   */
  public function userInfoUpdateremark($openid, $remark){
    $data = array('openid' => $openid, 'remark' => $remark);
    return $this->api('user/info/updateremark', $data);
  }

  /**
   * 获取指定用户的详细信息
   * @param string $openid 用户的openid
   * @param string $lang   需要获取数据的语言
   */
  public function userInfo($openid, $lang = 'zh_CN'){
    $param = array('openid' => $openid, 'lang' => $lang);
    return $this->api('user/info', '', 'GET', $param);
  }

  /**
   * 获取关注者列表
   * @param string $next_openid 下一个openid，在用户数大于10000时有效
   * @return array 用户列表
   */
  public function userGet($next_openid = ''){
    $param = array('next_openid' => $next_openid);
    return $this->api('user/get', '', 'GET', $param);
  }

  /**
   * 创建自定义菜单
   * @param array $button 符合规则的菜单数组，规则参见微信手册
   */
  public function menuCreate($data){
    return $this->api('menu/create', $data);
  }

  /**
   * 获取所有的自定义菜单
   * @return array  自定义菜单数组
   */
  public function menuGet(){
    return $this->api('menu/get', '', 'GET');
  }

  /**
   * 删除自定义菜单
   */
  public function menuDelete(){
    return $this->api('menu/delete', '', 'GET');
  }

  /**
   * 创建二维码，可创建指定有效期的二维码和永久二维码
   * @param integer $scene_id 二维码参数
   * @param integer $expire_seconds 二维码有效期，0-永久有效
   */
  public function qrcodeCreate($scene_id, $expire_seconds = 0){
    $data = array();

    if(is_numeric($expire_seconds) && $expire_seconds > 0){
      $data['expire_seconds'] = $expire_seconds;
      $data['action_name'] = self::QR_SCENE;
    }else{
      $data['action_name'] = self::QR_LIMIT_SCENE;
    }

    $data['action_info']['scene']['scene_id'] = $scene_id;
    return $this->api('qrcode/create', $data);
  }

  /**
   * 根据ticket获取二维码URL
   * @param string $ticket 通过 qrcodeCreate接口获取到的ticket
   * @return string         二维码URL
   */
  public function showqrcode($ticket){
    return "{$this->qrcodeURL}/showqrcode?ticket={$ticket}";
  }

  /**
   * 长链接转短链接
   * @param string $long_url 长链接
   * @return string           短链接
   */
  public function shorturl($long_url){
    $data = array(
      'action' => 'long2short',
      'long_url' => $long_url
    );
    return $this->api('shorturl', $data);
  }

  /**
   * 调用微信api获取响应数据
   * @param string $name   API名称
   * @param string $data   POST请求数据
   * @param string $method 请求方式
   * @param string $param  GET请求参数
   * @return array          api返回结果
   */
  protected function api($name, $data = '', $method = 'POST', $param = ''){
    $access_token = $this->getAccessToken();
    $this->accessToken = $access_token['access_token'];
    $params = array('access_token' => $this->accessToken);
    if(!empty($param) && is_array($param)){
      $params = array_merge($params, $param);
    }

    $url = "{$this->apiURL}/{$name}";
    if(!empty($data)){
      //保护中文，微信api不支持中文转义的json结构
      array_walk_recursive($data, function(&$value){
        $value = urlencode($value);
      });
      $data = urldecode(json_encode($data));
    }

    $data = self::http($url, $params, $data, $method);
    return json_decode($data, true);
  }

  /**
   * 发送HTTP请求方法，目前只支持CURL发送请求
   * @param string $url    请求URL
   * @param array  $param  GET参数数组
   * @param array  $data   POST的数据，GET请求时该参数无效
   * @param string $method 请求方法GET/POST
   * @return array          响应数据
   */
  protected static function http($url, $param, $data = '', $method = 'GET'){
    $opts = array(
      CURLOPT_TIMEOUT => 30,
      CURLOPT_RETURNTRANSFER => 1,
      CURLOPT_SSL_VERIFYPEER => false,
      CURLOPT_SSL_VERIFYHOST => false,
    );

    /* 根据请求类型设置特定参数 */
    $opts[CURLOPT_URL] = $url.'?'.http_build_query($param);

    if(strtoupper($method) == 'POST'){
      $opts[CURLOPT_POST] = 1;
      $opts[CURLOPT_POSTFIELDS] = $data;

      if(is_string($data)){ //发送JSON数据
        $opts[CURLOPT_HTTPHEADER] = array(
          'Content-Type: application/json; charset=utf-8',
          'Content-Length: '.strlen($data),
        );
      }
    }

    /* 初始化并执行curl请求 */
    $ch = curl_init();
    curl_setopt_array($ch, $opts);
    $data = curl_exec($ch);
    $error = curl_error($ch);
    curl_close($ch);

    //发生错误，抛出异常
    if($error)
      throw new \Exception('请求发生错误：'.$error);

    return $data;
  }

  /**
   * 构造文本信息
   * @param string $content 要回复的文本
   */
  private static function text($content){
    $data['content'] = $content;
    return $data;
  }

  /**
   * 构造图片信息
   * @param integer $media 图片ID
   */
  private static function image($media){
    $data['media_id'] = $media;
    return $data;
  }

  /**
   * 构造音频信息
   * @param integer $media 语音ID
   */
  private static function voice($media){
    $data['media_id'] = $media;
    return $data;
  }

  /**
   * 构造视频信息
   * @param array $video 要回复的视频 [视频ID，标题，说明]
   */
  private static function video($video){
    $data = array();
    list(
        $data['media_id'],
        $data['title'],
        $data['description'],
        ) = $video;

    return $data;
  }

  /**
   * 构造音乐信息
   * @param array $music 要回复的音乐[标题，说明，链接，高品质链接，缩略图ID]
   */
  private static function music($music){
    $data = array();
    list(
        $data['title'],
        $data['description'],
        $data['musicurl'],
        $data['hqmusicurl'],
        $data['thumb_media_id'],
        ) = $music;

    return $data;
  }

  /**
   * 构造图文信息
   * @param array $news 要回复的图文内容
   * [    
   *      0 => 第一条图文信息[标题，说明，图片链接，全文连接]，
   *      1 => 第二条图文信息[标题，说明，图片链接，全文连接]，
   *      2 => 第三条图文信息[标题，说明，图片链接，全文连接]， 
   * ]
   */
  private static function news($news){
    $articles = array();
    foreach($news as $key => $value){
      list(
          $articles[$key]['title'],
          $articles[$key]['description'],
          $articles[$key]['url'],
          $articles[$key]['picurl']
          ) = $value;

      if($key >= 9)
        break; //最多只允许10条图文信息
    }

    $data['articles'] = $articles;
    return $data;
  }

  /**
   * 发送微信模板消息
   * @author Max.Yu <max@jipu.com>
   */
  public function sendTplMsg($data = array()){
    $token = $this->getAccessToken();
    $res = $this->http($this->sendTplMsgURL, array('access_token' => $token['access_token']), json_encode($data), 'POST');
    return $res;
  }
  
  /**
   * 获取带参数的永久二维码票据与URL
   * @author Justin
   */
  public function getQrcodeTicket($scene_id = 0){
    if($scene_id){
      $data = array(
        'action_name' => 'QR_LIMIT_SCENE',
        'action_info' => array(
          'scene' => array(
            'scene_id' => $scene_id
          )
        )
      );
      $token = $this->getAccessToken();
      $res = $this->http($this->getQrcodeTicketUrl, array('access_token' => $token['access_token']), json_encode($data), 'POST');
      return $res;
    }
  }
  
}
